package com.csx.framework.springboot.sample.aop.aspect;

import com.alibaba.fastjson.JSON;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Enumeration;
import java.util.HashMap;

/**
 * <p> 本案例是 Spring Aop Demo的核心类,主要完成的功能是 在一个 http 请求前后 获取请求基本信息,并根据这些信息记录相关的日志,
 * 且使用的是 切面表达式来实现指定类置入逻辑
 * <p> 编织切面生成代理类的核心作用类:
 *  <li> {@link AnnotationAwareAspectJAutoProxyCreator} implements {@link AbstractAutoProxyCreator}
 *  <li> {@link AbstractAutoProxyCreator#postProcessBeforeInstantiation(Class, String)} 扫描切面类中的切面方法,包装为 Aspect
 *  <li> {@link AbstractAutoProxyCreator#postProcessAfterInitialization(Object, String)} 将切面应用于目标类，生成代理类
 * <p> 切面方法执行顺序
 * <p> 1. @Around: 执行原始方法之前的代码
 * <p> 2. @Before: 执行原始方法之前触发执行
 * <p> 3. proceedingJoinPoint.proceed(): 执行原始方法
 * <p> 4. @AfterReturning: 正常执行原始方法之后,此时可以获取方法执行结果
 * <p> 5. @AfterThrowing:  异常执行原始方法之后,此时可以获取方法执行过程中发生的异常信息. 相当于 try..catch..finally中的 catch 代码块
 * <p> 6. @After: 执行原始方法之后,无论异常与否,都会进入到此方法,相当于 try..catch..finally中的 finally 代码块
 * <p> 7. @Around 执行完 @After 之后会执行 doAround 中 proceedingJoinPoint.proceed() 之后的代码。正常执行则执行后续代码;出现异常则执行 catch 中的代码,
 *  最终都会执行 finally 中的代码
 * @author cuisongxu
 * @date 2024/12/19 周四 11:44
 */
@Aspect
@Component
public class WebLogAspect {

    private final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    private final ThreadLocal<Long> startTime = new ThreadLocal<>();

    /**
     * 表达式说明:
     * <li> public: 表示匹配访问修饰符为 public 的 方法。
     * <li> *: 表示返回类型化为任意类型,它匹配所有类型的返回值
     * <li> com.csx.framework.springboot.sample.aop.controller..*: 表示匹配
     *  com.csx.framework.springboot.sample.aop.controller 包下的类。 .. 表示匹配该包及其所有子包(递归匹配)
     * <li> *.*(..): 表示匹配该类中所有方法, (..) 表示任意参数的方法
     */
    @Pointcut("execution(public * com.csx.framework.springboot.sample.aop.controller..*.*(..))")
    public void pointcut(){};

    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) {
        logger.info("进入 doBefore");
        startTime.set(System.currentTimeMillis());

        // 获取请求数据
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert servletRequestAttributes != null;
        HttpServletRequest request = servletRequestAttributes.getRequest();
        // 另外一种获取 request 的方式
        // HttpServletRequest request2 = (HttpServletRequest) servletRequestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        HttpSession httpSession = (HttpSession) servletRequestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);

        String remoteAddr = request.getRemoteAddr();
        StringBuffer requestURL = request.getRequestURL();
        String method = request.getMethod();
        Enumeration<String> enumeration = request.getHeaderNames();

        // 获取连接点数据
        String declaringTypeName = joinPoint.getSignature().getDeclaringTypeName();
        String name = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        Object body = null;
        if(args != null && args.length > 0) {
            body = args[0];
        }


        // 打印数据
        logger.info("====================================start==============================================");
        logger.info("请求URL: " + requestURL.toString());
        logger.info("请求方法: " + method);
        logger.info("请求IP: " + remoteAddr);
        assert httpSession != null;
        logger.info("请求 SESSION ID: " + httpSession.getId());

        HashMap<Object, Object> headerMap = new HashMap<>();
        while(enumeration.hasMoreElements()) {
            String headerName = enumeration.nextElement();
            headerMap.put(headerName, request.getHeader(headerName));
        }
        String headerJsonStr = JSON.toJSONString(headerMap);
        if(headerJsonStr.length() > 0) {
            logger.info("请求头: {}", headerJsonStr);
        }


        logger.info("类方法: " + declaringTypeName + "." + name);
        try {
            logger.info("请求参数体: {}", JSON.toJSONString(body));
        }catch (Exception e){
            logger.error("REQUEST BODY PARSE ERROR!");
        }
    }

    /**
     * 后置通知
     * @param result
     */
    @AfterReturning(returning = "result", pointcut = "pointcut()")
    public void doAfterReturning(Object result) {
        logger.info("进入 doAfterReturning");
        // 处理完请求，返回内容
        logger.info("响应结果: " + result);
        logger.info("花费时间: {} ms", (System.currentTimeMillis() - startTime.get()));
    }

    /**
     * 异常通知
     * @param joinPoint
     * @param ex
     */
    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable ex) {
        logger.info("进入 doAfterThrowing");
        logger.info("捕获异常: ",ex);
    }

    /**
     * 最终通知
     */
    @After("pointcut()")
    public void doAfter() {
        logger.info("进入 doAfter");
        logger.info("============================================ End ===============================");
        // 每个请求之间空一行
        logger.info("");
    }

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) {
        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            logger.info("进入 doAround");
            // 执行原始方法
            result = proceedingJoinPoint.proceed();

            logger.info("进入 doAround,原始方法执行完毕");
            String resultStr = JSON.toJSONString(result);
            // 打印出参
            logger.info("doAround 中获取的 响应结果: {}", resultStr);

        } catch (Throwable e) {
            logger.info("doAround 中捕获异常: ",e);
        }finally {
            // 执行耗时
            logger.info("doAround 中记录的 花费时间 : {} ms", System.currentTimeMillis() - startTime);
        }
        return result;
    }

}
